/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.sondertara.common.bean;


import org.apache.commons.lang3.reflect.MethodUtils;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * An internally used helper class for storing introspection information about a bean
 * class.
 * </p>
 * <p>
 * This class is used by {@link PropertyUtilsBean}. When accessing bean properties via
 * reflection information about the properties available and their types and access
 * methods must be present. {@code PropertyUtilsBean} stores this information in a cache
 * so that it can be accessed quickly. The cache stores instances of this class.
 * </p>
 * <p>
 * This class mainly stores information about the properties of a bean class. Per default,
 * this is contained in {@code PropertyDescriptor} objects. Some additional information
 * required by the {@code BeanUtils} library is also stored here.
 * </p>
 *
 * @since 1.9.1
 */
class BeanIntrospectionData {
	/** An array with property descriptors for the managed bean class. */
	private final PropertyDescriptor[] descriptors;

	/** A map for remembering the write method names for properties. */
	private final Map<String, String> writeMethodNames;

	/**
	 * Creates a new instance of {@code BeanIntrospectionData} and initializes its
	 * completely.
	 *
	 * @param descs the array with the descriptors of the available properties
	 */
	public BeanIntrospectionData(final PropertyDescriptor[] descs) {
		this(descs, setUpWriteMethodNames(descs));
	}

	/**
	 * Creates a new instance of {@code BeanIntrospectionData} and allows setting the map
	 * with write method names. This constructor is mainly used for testing purposes.
	 *
	 * @param descs the array with the descriptors of the available properties
	 * @param writeMethNames the map with the names of write methods
	 */
	BeanIntrospectionData(final PropertyDescriptor[] descs, final Map<String, String> writeMethNames) {
		descriptors = descs;
		writeMethodNames = writeMethNames;
	}

	/**
	 * Returns the array with property descriptors.
	 *
	 * @return the property descriptors for the associated bean class
	 */
	public PropertyDescriptor[] getDescriptors() {
		return descriptors;
	}

	/**
	 * Returns the {@code PropertyDescriptor} for the property with the specified name. If
	 * this property is unknown, result is <b>null</b>.
	 *
	 * @param name the name of the property in question
	 * @return the {@code PropertyDescriptor} for this property or <b>null</b>
	 */
	public PropertyDescriptor getDescriptor(final String name) {
		for (final PropertyDescriptor pd : getDescriptors()) {
			if (name.equals(pd.getName())) {
				return pd;
			}
		}
		return null;
	}

	/**
	 * Returns the write method for the property determined by the given
	 * {@code PropertyDescriptor}. This information is normally available in the
	 * descriptor object itself. However, at least by the ORACLE implementation, the
	 * method is stored as a {@code SoftReference}. If this reference has been freed by
	 * the GC, it may be the case that the method cannot be obtained again. Then,
	 * additional information stored in this object is necessary to obtain the method
	 * again.
	 *
	 * @param beanCls the class of the affected bean
	 * @param desc the {@code PropertyDescriptor} of the desired property
	 * @return the write method for this property or <b>null</b> if there is none
	 */
	public Method getWriteMethod(final Class<?> beanCls, final PropertyDescriptor desc) {
		Method method = desc.getWriteMethod();
		if (method == null) {
			final String methodName = writeMethodNames.get(desc.getName());
			if (methodName != null) {
				method = MethodUtils.getAccessibleMethod(beanCls, methodName,
						desc.getPropertyType());
				if (method != null) {
					try {
						desc.setWriteMethod(method);
					} catch (final IntrospectionException e) {
						// ignore, in this case the method is not cached
					}
				}
			}
		}

		return method;
	}

	/**
	 * Initializes the map with the names of the write methods for the supported
	 * properties. The method names - if defined - need to be stored separately because
	 * they may get lost when the GC claims soft references used by the
	 * {@code PropertyDescriptor} objects.
	 *
	 * @param descs the array with the descriptors of the available properties
	 * @return the map with the names of write methods for properties
	 */
	private static Map<String, String> setUpWriteMethodNames(final PropertyDescriptor[] descs) {
		final Map<String, String> methods = new HashMap<>();
		for (final PropertyDescriptor pd : descs) {
			final Method method = pd.getWriteMethod();
			if (method != null) {
				methods.put(pd.getName(), method.getName());
			}
		}
		return methods;
	}
}